如何编写一个简单地内核模块

Linux给应用程序提供了丰富的api,但是有时候我们需要跟硬件交互,访问一些特权级信息,所以可以使用编写内核模块这种方式。
另外Linux是宏内核结构,效率非常高,没有微内核那样各个模块之间的通讯损耗,但是又不能方便的对内核进行改动,可扩展性和可维护性比较差,内核模块提供了一种动态加载代码的方式,弥补了宏内核的不足。

步骤

  • 首先需要xxx.c原文件存放代码,Makefile用来编译xxx.c文件。
  • 编写内核模块源文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    // lkm_example.c
    #include <linux/init.h> //必须包含,里面定义了__init和__exit两个宏,分别用来指定模块初始化函数和模块卸载函数
    #include <linux/module.h> //必须包含,定义了动态加载内核模块所需的必要信息
    #include <linux/kernel.h> //包含了内核常用API,比如内核打印函数printk()

    //__init会将lkm_example_init函数标记为初始化函数,模块被装载到内核时会调用该函数。
    static int __init lkm_example_init(void) {
    printk(KERN_INFO "Hello, World!\n"); //
    return 0;
    }

    //模块被卸载时被调用
    static void __exit lkm_example_exit(void) {
    printk(KERN_INFO "Goodbye, World!\n");
    }

    module_init(lkm_example_init); //引导内核加载模块
    module_exit(lkm_example_exit); //引导内核卸载模块

    MODULE_LICENSE("GPL"); //必选项 模块许可证,如果没有添加模块许可证,会收到内核被污染的警告
    MODULE_AUTHOR("YIFEI"); //可选 模块作者
    MODULE_DESCRIPTION("linux module"); //可选 模块描述
    MODULE_VERSION("0.01"); //可选项 模块版本
  • 编写Makefile文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    obj-m += lkm_example.o

    all:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
    #-C选项:此选项指定内核源码的位置,make在编译时将会进入内核源码目录,执行编译,编译完成时返回。
    #这个build/目录是一个软连接,链接到源码头文件的安装位置。
    #M=$(PWD):需要编译的模块源文件地址

    clean:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
  • 执行make编译模块

    1
    $ make
  • 装载模块

    1
    $ sudo insmod lkm_example.ko
  • 查看装载的模块

    1
    $ lsmod
  • 卸载模块

    1
    $ sudo rmmod lkm_example.ko
  • 查看打印的日志

    1
    2
    3
    $ sudo dmesg
    [75789.276382] Hello, World!
    [75789.307013] Goodbye, World!
  • 可以在Makefile最后添加以下代码,将测试流程自动化,每次只需执行 make test.

    1
    2
    3
    4
    5
    test:
    sudo dmesg -C
    sudo insmod lkm_example.ko
    sudo rmmod lkm_example.ko
    dmesg

其他知识点

  • 往内核模块传参数
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    static int pid = -1;
    module_param(pid,int,S_IRUGO);
    /*
    在内核模块中定义一个全局变量,然后用module_param声明一下
    参数一:表示参数的名字;
    参数二:表示参数的类型;
    参数三:表示参数的访问权限,S_IRUGO表示参数可以被所有人读取, 但是不能改变。
    #define S_IRWXU 00700
    #define S_IRUSR 00400
    #define S_IWUSR 00200
    #define S_IXUSR 00100
    #define S_IRWXG 00070
    #define S_IRGRP 00040
    #define S_IWGRP 00020
    #define S_IXGRP 00010
    #define S_IRWXO 00007
    #define S_IROTH 00004
    #define S_IWOTH 00002
    #define S_IXOTH 00001
    当往模块传数组类型的参数时
    module_param_array(name, type, num, perm);
    name:表示数组的名字;
    type:表示参数的类型;
    num :表示数组中元素数量;
    perm:表示参数的访问权限;
    */

模块间函数调用

  • ma.c

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    #include <linux/module.h>

    int b=0;

    void fun1(){
    int a=0;
    a++;
    b++;
    printk("%d %d \n",a,b);
    }

    EXPORT_SYMBOL(fun1);
  • ma.c的Makefile

    1
    2
    3
    4
    5
    6
    7
    obj-m += ma.o

    all:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

    clean:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
  • mb.c

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    #include <linux/module.h>
    #include <linux/init.h>
    #include <linux/kernel.h>

    extern void fun1();

    static int __init mb_init(void){
    printk("hello\n");
    fun1();
    }

    static int __exit mb_exit(void){
    printk("goodbye\n");
    }

    module_init(mb_init);
    module_exit(mb_exit);

    MODULE_LICENSE("GPL");
  • mb.c的Makefile

    1
    2
    3
    4
    5
    6
    7
    8
    9
    -m += mb.o

    KBUILD_EXTRA_SYMBOLS=/home/yifei/src/module_test/ma/Module.symvers #去该目录查找ma.ko的符号表

    all:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

    clean:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

执行过程:

1
2
3
4
5
6
7
8
9
10
11
12
#要先插入ma.ko模块,再插入mb.ko.删除模块时顺序相反。
cd ma
make
insmod ma.ko

cd ../mb
make
insmod mb.ko
dmesg

rmmod mb.ko
rmmod ma.ko

Q&A

  • printk()使用方法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    printk相比printf来说还多了个:日志级别的设置,用来控制printk打印的这条信息是否在终端上显示的,当日志级别的数值小于控制台级别时,printk要打印的信息才会在控制台打印出来,否则不会显示在控制台!

    在我们内核中一共有8种级别,他们分别为:
    #define KERN_EMERG "<0>" /* system is unusable */
    #define KERN_ALERT "<1>" /* action must be taken immediately */
    #define KERN_CRIT "<2>" /* critical conditions */
    #define KERN_ERR "<3>" /* error conditions */
    #define KERN_WARNING "<4>" /* warning conditions */
    #define KERN_NOTICE "<5>" /* normal but significant condition */
    #define KERN_INFO "<6>" /* informational */
    #define KERN_DEBUG "<7>" /* debug-level messages */
  • 执行make编译内核模块时遇到签名验证失败时,在Makefile开始添加:

    1
    CONFIG_MODULE_SIG=n	#关闭签名验证
  • 根据pid获取可执行文件的绝对路径
    https://www.cnblogs.com/ddk3000/p/5051111.html

参考

欢迎与我分享你的看法。
转载请注明出处:http://taowusheng.cn/